package com.blood.core.util.http;

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.stereotype.Component;

import javax.xml.bind.DatatypeConverter;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.List;
import java.util.TreeMap;

/**
 * 请求幂等性工具
 *
 * @author daoren
 * @date 2022/4/25 11:37
 */
@Slf4j
@Component
public class ReqDedUpHelper {
    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * 1000毫秒过期，1000ms内的重复请求会认为重复
     */
    private static final long expireTime = 1000;

    /**
     * 设置防重复过期key
     *
     * @param userId   用户id
     * @param method   接口路径/名称
     * @param dedupMD5 请求参数md5
     * @return
     */
    private boolean checkInfo(String userId, String method, String dedupMD5) {
        String key = new StringBuilder()
                .append("dedup:U=")
                .append(userId)
                .append("M=")
                .append(method)
                .append("P=")
                .append(dedupMD5)
                .toString();
        return setRedisKey(key);
    }

    /**
     * 设置防重复过期key
     *
     * @param method   接口路径/名称
     * @param dedupMD5 请求参数md5
     * @return
     */
    public boolean checkInfo(String method, String dedupMD5) {
        String key = new StringBuilder()
                .append("dedup:M=")
                .append(method)
                .append("P=")
                .append(dedupMD5)
                .toString();
        return setRedisKey(key);
    }

    private boolean setRedisKey(String key) {
        long expireAt = System.currentTimeMillis() + expireTime;
        String val = "expireAt@" + expireAt;
//        RedisTemplate redisTemplate1 = new RedisTemplate();
// NOTE:直接SETNX不支持带过期时间，所以设置+过期不是原子操作，极端情况下可能设置了就不过期了，后面相同请求可能会误以为需要去重，所以这里使用底层API，保证SETNX+过期时间是原子操作
        Boolean execute = (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection ->
                connection.set(key.getBytes(), val.getBytes(),
                        Expiration.milliseconds(expireTime),
                        RedisStringCommands.SetOption.SET_IF_ABSENT));

        final boolean isConsiderDup;
        if (execute != null && execute) {
            isConsiderDup = false;
        } else {
            isConsiderDup = true;
        }
        return isConsiderDup;
    }


    /**
     * @param reqJSON     请求的参数，这里通常是JSON
     * @param excludeKeys 请求参数里面要去除哪些字段再求摘要
     * @return 去除参数的MD5摘要
     */
    public String dedupParamMD5(final String reqJSON, String... excludeKeys) {
        String decreptParam = reqJSON;

        TreeMap paramTreeMap = JSONUtil.toBean(decreptParam, TreeMap.class);
        if (excludeKeys != null) {
            List<String> dedupExcludeKeys = Arrays.asList(excludeKeys);
            if (!dedupExcludeKeys.isEmpty()) {
                for (String dedupExcludeKey : dedupExcludeKeys) {
                    paramTreeMap.remove(dedupExcludeKey);
                }
            }
        }

        String paramTreeMapJSON = JSONUtil.toJsonStr(paramTreeMap);
        String md5deDupParam = jdkMD5(paramTreeMapJSON);
        log.debug("md5deDupParam = {}, excludeKeys = {} {}", md5deDupParam, Arrays.deepToString(excludeKeys), paramTreeMapJSON);
        return md5deDupParam;
    }

    private static String jdkMD5(String src) {
        String res = null;
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            byte[] mdBytes = messageDigest.digest(src.getBytes());
            res = DatatypeConverter.printHexBinary(mdBytes);
        } catch (Exception e) {
            log.error("", e);
        }
        return res;
    }

    /**
     * 验证请求是否重复
     *
     * @param reqJsonStr 请求参数
     * @param userId     用户
     * @param method     接口名
     * @return
     */
    public boolean checkRepeat(String reqJsonStr, String userId, String method) {
        //计算请求参数摘要，其中剔除里面请求时间的干扰
        String dedupMD5 = new ReqDedUpHelper().dedupParamMD5(reqJsonStr);
        return checkInfo(userId, method, dedupMD5);
    }

    /**
     * 验证请求是否重复
     *
     * @param userId 用户
     * @param method 接口名
     * @return
     */
    public boolean checkRepeat(String userId, String method) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.putOpt("userId", userId);
        jsonObject.putOpt("method", method);
        //计算请求参数摘要，其中剔除里面请求时间的干扰
        String dedupMD5 = new ReqDedUpHelper().dedupParamMD5(JSONUtil.toJsonStr(jsonObject));
        return checkInfo(userId, method, dedupMD5);
    }

}
